"""
  WELCOME TO...

         █
         █
     ▄   █   ▄                                █     █
     ▀▀█████▀▀      ▄▀▀▀▄ ▄▀▀▀▄    ▄▀▀▀▀▄█  ▀▀█▀▀▀▀▀█▀▀   ▄▀▀▀▀▄    ▄▀▀
   ▀█▄       ▄█▀   █     █     █  █      █    █     █    █▄▄▄▄▄▄█  █
     ▀█▄   ▄█▀     █     █     █  █      █    █     █    █         █
  ▄██▀▀█   █▀▀██▄  █     █     █   ▀▄▄▄▄▀█    ▀▄▄   ▀▄▄   ▀▄▄▄▄▀   █
 ▀▀    █   █    ▀▀

# This application is a gui shell client of chip-tool, an Ubuntu host with chip-tool, ble adapter and the necessary
# env is needed.
# Author: Yan Wei，  May10,2023
"""
from PyQt6.QtCore import Qt, QSize, QDateTime, QThread, pyqtSignal
from PyQt6.QtWidgets import QMainWindow, QApplication, QFileDialog, QDialog, QPushButton, QVBoxLayout, QHBoxLayout, QLabel
from PyQt6.QtGui import QIcon, QWheelEvent
from ui.ui import Ui_MainWindow
import paramiko
from colorama import init
import threading
import time
import re
import os
import configparser
import gevent
import traceback
from enum import Enum
import qtawesome as qta   # run qta-browser from a shell to start the browser
import queue
from scripts.pigeonhole import PigeonBreeder
from scripts.message_parser import msg_parser
from scripts.matter_enum import *
from scripts.ssh_channel_send import cmd_send_out


class MatterController(QMainWindow):
    def __init__(self):
        super(MatterController, self).__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.showMaximized()
        self.ui.log_printer.document().setMaximumBlockCount(100000)

        # Pigeon plan
        # All the ui_operation actions should be processed by a unified queue, the queue object is self.dispatcher
        self.dispatcher = queue.Queue()  # init Queue object
        self.pigeon_breeder = PigeonBreeder(ui=self.ui, dispatcher=self.dispatcher)
        self.pigeon_breeder.order.connect(self.ui_operation)
        self.pigeon_breeder.start()  # init Queue receiving
        self.dispatcher.put((self.ui.statusbar, 'QStatusBar.showMessage', 'Ready ?! Goo          **********系统 > 屏幕 >'
                                                                          ' 缩放和布局 > 100%(推荐)**********'))

        self.ui.clear_logs.clicked.connect(lambda: self.ui.log_printer.clear())
        self.ui.clear_logs.clicked.connect(lambda: self.ui.keynote.clear())
        # Icon setting
        self.ui.connect.setIcon(qta.icon('mdi.lan-connect', color='green'))
        self.ui.disconnect.setIcon(qta.icon('mdi.lan-disconnect', color='red'))
        self.ui.disconnect.setEnabled(False)
        self.ui.h_Red.setIcon(qta.icon('ph.lightbulb-fill', color='red'))
        self.ui.h_Green.setIcon(qta.icon('ph.lightbulb-fill', color='green'))
        self.ui.h_Blue.setIcon(qta.icon('ph.lightbulb-fill', color='blue'))
        self.ui.h_Yellow.setIcon(qta.icon('ph.lightbulb-fill', color='yellow'))
        self.ui.h_Deeppink.setIcon(qta.icon('ph.lightbulb-fill', color='deep pink'))
        self.ui.h_Purple.setIcon(qta.icon('ph.lightbulb-fill', color='purple'))
        self.ui.h_Orange.setIcon(qta.icon('ph.lightbulb-fill', color='orange'))
        self.ui.h_Teal.setIcon(qta.icon('ph.lightbulb-fill', color='teal'))
        self.ui.h_MidnightBlue.setIcon(qta.icon('ph.lightbulb-fill', color='midnight blue'))
        self.ui.xred.setIcon(qta.icon('ri.lightbulb-fill', color='red'))
        self.ui.xgreen.setIcon(qta.icon('ri.lightbulb-fill', color='green'))
        self.ui.xblue.setIcon(qta.icon('ri.lightbulb-fill', color='blue'))
        self.ui.xpurple.setIcon(qta.icon('ri.lightbulb-fill', color='purple'))
        self.ui.xorange.setIcon(qta.icon('ri.lightbulb-fill', color='orange'))
        self.ui.xyellow.setIcon(qta.icon('ri.lightbulb-fill', color='yellow'))
        self.ui.xdeeppink.setIcon(qta.icon('ri.lightbulb-fill', color='deep pink'))
        self.ui.xTeal.setIcon(qta.icon('ri.lightbulb-fill', color='teal'))
        self.ui.xMidnightBlue.setIcon(qta.icon('ri.lightbulb-fill', color='midnight blue'))
        self.ui.xGold.setIcon(qta.icon('ri.lightbulb-fill', color='gold'))
        self.ui.level_move_down.setIcon(qta.icon('fa5s.sort-amount-down', color='green'))
        self.ui.level_move_up.setIcon(qta.icon('fa5s.sort-amount-up', color='green'))
        self.ui.onoff.setIcon(qta.icon('mdi.lightbulb-off'))
        self.ui.toggle.setIcon(qta.icon('mdi.lightbulb-off'))
        self.onoff_status = True
        self.ui.on_with_timed_off.setIcon(qta.icon('mdi6.clock-time-eight-outline'))
        self.ui.off_with_effect.setIcon(qta.icon('ri.lightbulb-flash-line'))
        self.ui.on_with_recall_global_scene.setIcon(qta.icon('mdi.order-bool-descending-variant'))
        self.ui.mv_to_level.setIcon(qta.icon('fa.sliders', color='green'))
        self.ui.step_down.setIcon(qta.icon('mdi.download-multiple', color='green'))
        self.ui.step_up.setIcon(qta.icon('mdi.upload-multiple', color='green'))
        self.ui.move_to_hue.setIcon(qta.icon('mdi.alpha-h-circle', color='green'))
        self.ui.move_hue.setIcon(qta.icon('mdi.alpha-h-circle', color='green'))
        self.ui.step_hue.setIcon(qta.icon('mdi.alpha-h-circle', color='green'))
        self.ui.move_to_sat.setIcon(qta.icon('mdi.alpha-s-circle', color='green'))
        self.ui.move_sat.setIcon(qta.icon('mdi.alpha-s-circle', color='green'))
        self.ui.step_sat.setIcon(qta.icon('mdi.alpha-s-circle', color='green'))
        self.ui.commissioner_reset.setIcon(qta.icon('mdi.lock-reset', color='green'))
        self.ui.movetocolortemp.setIcon(qta.icon('mdi.temperature-kelvin', color='green'))
        self.ui.write_basic.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_basic.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.cmd_send.setIcon(qta.icon('ri.send-plane-fill', color='green'))
        self.ui.path_setting.setIcon(qta.icon('ri.settings-3-fill', color='green'))
        self.ui.write_onoff.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_onoff.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.write_level.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_level.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.write_cc.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_cc.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.write_identify.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_identify.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.write_ota.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_ota.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.write_discriptor.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_discriptor.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.write_general_commissioning.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_general_commissioning.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.write_grpkey.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_grpkey.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.write_sw_diagnostics.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_sw_diagnostics.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.write_general_diagnostics.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_general_diagnostics.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.write_wifinetworkdiagnostics.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.read_wifinetworkdiagnostics.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.read_grpkey.setIcon(qta.icon('mdi.eye-check', color='green'))
        self.ui.write_grpkey.setIcon(qta.icon('ph.pen-nib-bold', color='green'))
        self.ui.identify_id.setIcon(qta.icon('ei.idea', color='gold'))
        # ssh connect & disconnect
        init(autoreset=True)
        self.ssh_client, self.ssh_session, self.ssh_channel = None, None, None
        self.ssh_msg_collector = None
        self.ssh_connect_flag = False
        self.ui.connect.clicked.connect(self.connect_v2)
        self.ui.disconnect.clicked.connect(self.disconnect)
        self.user_name, self.password = None, None

        # dev list
        self.cfg = None
        self.cfg_path = './cfg/dev.ini'
        if not os.path.exists(self.cfg_path):
            os.popen('echo [dev_node_id] > ./cfg/dev.ini')
        self.items, self.dev_num = None, 0
        self.node_list = []
        self.paa_cfg_file = './cfg/paa_path.ini'
        # some
        self.current_attribute = None
        # Version Dialog
        self.dialog = QDialog()
        self.dialog.resize(400, 100)
        self.yes = QPushButton("Close")
        self.dialog.setWindowTitle("Application Version")
        self.ui.actionCurrentVersion.triggered.connect(self.version_dialog)
        self.yes.clicked.connect(self.dialog.close)

        # ____________________________________________________________________________________________________________ #
        # parser 2.0
        self.parser = None
        self.endpoint, self.cluster_id, self.attribute_id, self.command_id = None, None, None, None
        self.unique_id, self.cmd_id, self.attrib_name, self.cmd_name, self.status = None, None, None, None, None
        self.current_cmd, self.current_dut = None, None
        self.ocw_timer, self.revoked = '0', False
        self.cm_step = None
        # ------------------------------------------------------------------------------------------------------------ #

        # mgmt page
        self.chip_app_path = f'/home/{self.user_name}/apps/'
        self.path_confirmed = False
        self.path_confirm_dialog = QDialog()
        self.path_confirm_dialog.resize(400, 100)
        self.path_confirm = QPushButton("Confirm")
        self.path_cancel = QPushButton("Cancel")
        self.path_confirm_dialog.setWindowTitle('Chip-tool App path Setting')
        self.path_cancel.clicked.connect(self.path_confirm_dialog.close)
        self.path_confirm.clicked.connect(lambda: self.cmd_send('app_path_confirmed'))
        self.ui.path_setting.clicked.connect(lambda: self.cmd_send('app_path_setting'))
        self.ui.app_path.returnPressed.connect(lambda: self.cmd_send('app_path_setting'))
        self.ui.cmd_input.returnPressed.connect(lambda: self.cmd_send(self.ui.cmd_input.text()))
        self.ui.cmd_input.returnPressed.connect(lambda: self.ui.cmd_input.setText(f'{self.chip_app_path}chip-tool'))
        self.ui.cmd_send.clicked.connect(lambda: self.cmd_send(self.ui.cmd_input.text()))
        self.ui.cmd_send.clicked.connect(lambda: self.ui.cmd_input.setText(f'{self.chip_app_path}chip-tool'))
        self.ui.dut_pairing.clicked.connect(self.dut_pairing)
        self.ui.pairing_mode.currentIndexChanged.connect(self.pair_mode)
        self.ui.unpair.clicked.connect(self.dut_unpair)
        self.ui.read_basic.clicked.connect(lambda: self.cmd_send('basic-read'))
        self.ui.commissioner_reset.clicked.connect(lambda: self.cmd_send('rm -rf /tmp/*.*'))
        self.ui.write_basic.clicked.connect(lambda: self.attrib_write(cluster_name='basicinformation',
                                                                attrib_name=self.ui.attrib_id_basic.currentText(),
                                                                value=self.ui.attrib_value_basic.text(),
                                                                endpoint='0'))
        self.ui.payload_parse.clicked.connect(lambda: self.cmd_send('payload_parse'))
        # ctrl cluster
        self.ui.onoff.clicked.connect(lambda: self.cmd_send(self.ui.onoff.text()))
        self.ui.toggle.clicked.connect(lambda: self.cmd_send('toggle'))
        self.ui.on_with_timed_off.clicked.connect(lambda: self.cmd_send('on_with_timed_off'))
        self.ui.off_with_effect.clicked.connect(lambda: self.cmd_send('off_with_effect'))
        self.ui.on_with_recall_global_scene.clicked.connect(lambda: self.cmd_send('on_with_recall_global_scene'))
        self.ui.mv_to_level.clicked.connect(lambda: self.cmd_send('move_to_level'))
        self.ui.move_to_level_with_on_off.clicked.connect(lambda: self.cmd_send('move_to_level_with_on_off'))
        self.ui.read_level.clicked.connect(lambda: self.cmd_send('read_level'))
        self.ui.read_onoff.clicked.connect(lambda: self.cmd_send('read_onoff'))
        self.ui.level_move_up.clicked.connect(lambda: self.cmd_send('move_up'))
        self.ui.level_move_down.clicked.connect(lambda: self.cmd_send('move_down'))
        self.ui.o_move_up.clicked.connect(lambda: self.cmd_send('move_with_on_off_up'))
        self.ui.o_move_down.clicked.connect(lambda: self.cmd_send('move_with_on_off_down'))
        self.ui.step_up.clicked.connect(lambda: self.cmd_send('step_up'))
        self.ui.step_down.clicked.connect(lambda: self.cmd_send('step_down'))
        self.ui.o_step_up.clicked.connect(lambda: self.cmd_send('step_up_with_on_off'))
        self.ui.o_step_down.clicked.connect(lambda: self.cmd_send('step_down_with_on_off'))
        self.ui.stop.clicked.connect(lambda: self.cmd_send('stop'))
        self.ui.stop_with_on_off.clicked.connect(lambda: self.cmd_send('stop_with_on_off'))
        self.ui.movetocolortemp.clicked.connect(lambda: self.cmd_send('move_to_color_temperature'))
        self.ui.read_cc.clicked.connect(lambda: self.cmd_send('read_cc'))
        self.ui.move_to_hue.clicked.connect(lambda: self.cmd_send('move_to_hue'))
        self.ui.move_hue.clicked.connect(lambda: self.cmd_send('move_hue'))
        self.ui.step_hue.clicked.connect(lambda: self.cmd_send('step_hue'))
        self.ui.move_to_sat.clicked.connect(lambda: self.cmd_send('move_to_saturation'))
        self.ui.move_to_hue.clicked.connect(lambda: self.cmd_send('move_to_hue'))
        self.ui.move_sat.clicked.connect(lambda: self.cmd_send('move_saturation'))
        self.ui.step_sat.clicked.connect(lambda: self.cmd_send('step_saturation'))
        self.ui.stop_move.clicked.connect(lambda: self.cmd_send('stop_move_step'))
        self.ui.en_move_to_hue.clicked.connect(lambda: self.cmd_send('en_move_to_hue'))
        self.ui.en_move_hue.clicked.connect(lambda: self.cmd_send('en_move_hue'))
        self.ui.en_step_hue.clicked.connect(lambda: self.cmd_send('en_step_hue'))
        self.ui.en_move_to_hue_sat.clicked.connect(lambda: self.cmd_send('en_move_to_hue_sat'))
        self.ui.movetocolor.clicked.connect(lambda: self.cmd_send('move_to_color'))
        self.ui.move_color.clicked.connect(lambda: self.cmd_send('move_color'))
        self.ui.step_color.clicked.connect(lambda: self.cmd_send('step_color'))
        self.ui.movetohueandsat.clicked.connect(lambda: self.cmd_send('move_to_hue_and_saturation'))
        self.ui.move_cct.clicked.connect(lambda: self.cmd_send('move_color_temperature'))
        self.ui.step_cct.clicked.connect(lambda: self.cmd_send('step_color_temperature'))
        self.ui.color_loop_set.clicked.connect(lambda: self.cmd_send('color_loop_set'))
        self.ui.write_onoff.clicked.connect(lambda: self.attrib_write(cluster_name='onoff',
                                                                      attrib_name=self.ui.attrib_id_onoff.currentText(),
                                                                      value=self.ui.attrib_value_onoff.text(),
                                                                      endpoint='1'))
        self.ui.write_level.clicked.connect(lambda: self.attrib_write(cluster_name='levelcontrol',
                                                                      attrib_name=self.ui.attrib_id_level.currentText(),
                                                                      value=self.ui.attrib_value_level.text(),
                                                                      endpoint='1'))
        self.ui.write_cc.clicked.connect(lambda: self.attrib_write(cluster_name='colorcontrol',
                                                                   attrib_name=self.ui.attrib_id_cc.currentText(),
                                                                   value=self.ui.attrib_value_cc.text(),
                                                                   endpoint='1'))
        # short-cut
        self.ui.h_Red.clicked.connect(lambda: self.cmd_send('move_to_hue_red'))
        self.ui.h_Yellow.clicked.connect(lambda: self.cmd_send('move_to_hue_yellow'))
        self.ui.h_Blue.clicked.connect(lambda: self.cmd_send('move_to_hue_blue'))
        self.ui.h_Teal.clicked.connect(lambda: self.cmd_send('move_to_hue_teal'))
        self.ui.h_Green.clicked.connect(lambda: self.cmd_send('move_to_hue_green'))
        self.ui.h_Purple.clicked.connect(lambda: self.cmd_send('move_to_hue_purple'))
        self.ui.h_Orange.clicked.connect(lambda: self.cmd_send('move_to_hue_orange'))
        self.ui.h_MidnightBlue.clicked.connect(lambda: self.cmd_send('move_to_hue_midnightblue'))
        self.ui.h_Deeppink.clicked.connect(lambda: self.cmd_send('move_to_hue_deeppink'))
        self.ui.xred.clicked.connect(lambda: self.cmd_send('move_to_color_red'))
        self.ui.xgreen.clicked.connect(lambda: self.cmd_send('move_to_color_green'))
        self.ui.xblue.clicked.connect(lambda: self.cmd_send('move_to_color_blue'))
        self.ui.xpurple.clicked.connect(lambda: self.cmd_send('move_to_color_purple'))
        self.ui.xorange.clicked.connect(lambda: self.cmd_send('move_to_color_orange'))
        self.ui.xyellow.clicked.connect(lambda: self.cmd_send('move_to_color_yellow'))
        self.ui.xdeeppink.clicked.connect(lambda: self.cmd_send('move_to_color_deeppink'))
        self.ui.xMidnightBlue.clicked.connect(lambda: self.cmd_send('move_to_color_midnightblue'))
        self.ui.xTeal.clicked.connect(lambda: self.cmd_send('move_to_color_teal'))
        self.ui.echo_red.clicked.connect(lambda: self.cmd_send('echo_red'))
        self.ui.echo_red.setIcon(QIcon(r'.\images\echo_red.png'))
        self.ui.echo_red.setIconSize(QSize(90, 28))
        self.ui.echo_crimson.clicked.connect(lambda: self.cmd_send('echo_crimson'))
        self.ui.echo_crimson.setIcon(QIcon(r'.\images\echo_crimson.jpg'))
        self.ui.echo_crimson.setIconSize(QSize(90, 28))
        self.ui.echo_salmon.clicked.connect(lambda: self.cmd_send('echo_salmon'))
        self.ui.echo_salmon.setIcon(QIcon(r'.\images\echo_salmon.jpg'))
        self.ui.echo_salmon.setIconSize(QSize(90, 28))
        self.ui.echo_orange.clicked.connect(lambda: self.cmd_send('echo_orange'))
        self.ui.echo_orange.setIcon(QIcon(r'.\images\echo_orange.jpg'))
        self.ui.echo_orange.setIconSize(QSize(90, 28))
        self.ui.echo_magenta.clicked.connect(lambda: self.cmd_send('echo_magenta'))
        self.ui.echo_magenta.setIcon(QIcon(r'.\images\echo_magenta.jpg'))
        self.ui.echo_magenta.setIconSize(QSize(90, 28))
        self.ui.echo_gold.clicked.connect(lambda: self.cmd_send('echo_gold'))
        self.ui.echo_gold.setIcon(QIcon(r'.\images\echo_gold.jpg'))

        self.ui.echo_gold.setIconSize(QSize(90, 28))
        self.ui.echo_yellow.clicked.connect(lambda: self.cmd_send('echo_yellow'))
        self.ui.echo_yellow.setIcon(QIcon(r'.\images\echo_yellow.jpg'))
        self.ui.echo_yellow.setIconSize(QSize(90, 28))
        self.ui.echo_green.clicked.connect(lambda: self.cmd_send('echo_green'))
        self.ui.echo_green.setIcon(QIcon(r'.\images\echo_green.jpg'))
        self.ui.echo_green.setIconSize(QSize(90, 28))
        self.ui.echo_turquoise.clicked.connect(lambda: self.cmd_send('echo_turquoise'))
        self.ui.echo_turquoise.setIcon(QIcon(r'.\images\echo_turquoise.jpg'))
        self.ui.echo_turquoise.setIconSize(QSize(90, 28))
        self.ui.echo_pink.clicked.connect(lambda: self.cmd_send('echo_pink'))
        self.ui.echo_pink.setIcon(QIcon(r'.\images\echo_pink.jpg'))
        self.ui.echo_pink.setIconSize(QSize(90, 28))
        self.ui.echo_cyan.clicked.connect(lambda: self.cmd_send('echo_cyan'))
        self.ui.echo_cyan.setIcon(QIcon(r'.\images\echo_cyan.jpg'))
        self.ui.echo_cyan.setIconSize(QSize(90, 28))
        self.ui.echo_skyblue.clicked.connect(lambda: self.cmd_send('echo_skyblue'))
        self.ui.echo_skyblue.setIcon(QIcon(r'.\images\echo_skyblue.jpg'))
        self.ui.echo_skyblue.setIconSize(QSize(90, 28))
        self.ui.echo_blue.clicked.connect(lambda: self.cmd_send('echo_blue'))
        self.ui.echo_blue.setIcon(QIcon(r'.\images\echo_blue.jpg'))
        self.ui.echo_blue.setIconSize(QSize(90, 28))
        self.ui.echo_purple.clicked.connect(lambda: self.cmd_send('echo_purple'))
        self.ui.echo_purple.setIcon(QIcon(r'.\images\echo_purple.jpg'))
        self.ui.echo_purple.setIconSize(QSize(90, 28))
        self.ui.echo_lavender.clicked.connect(lambda: self.cmd_send('echo_lavender'))
        self.ui.echo_lavender.setIcon(QIcon(r'.\images\echo_lavender.jpg'))
        self.ui.echo_lavender.setIconSize(QSize(90, 28))
        self.ui.echo_white.clicked.connect(lambda: self.cmd_send('echo_white'))
        # identify
        self.ui.identify_id.clicked.connect(lambda: self.cmd_send('identify'))
        self.ui.effect_trigger.clicked.connect(lambda: self.cmd_send('identify_trigger_effect'))
        self.ui.read_identify.clicked.connect(lambda: self.cmd_send('identify_read'))
        # OTA
        self.ota_bin_file = None
        self.ota_queried = False
        self.ota_provider_app_activated = False
        self.ota_session_flag = True
        self.ota_provider_ssh = None
        self.ui.tabWidget.currentChanged.connect(self.ota_file_query)
        self.ui.ota_file_import.clicked.connect(lambda: self.cmd_send('ota_file_select'))
        self.ui.active_ota_file.clicked.connect(lambda: self.cmd_send('active_ota_file'))
        self.ui.write_acl.clicked.connect(lambda: self.cmd_send('ota_write_acl'))
        self.ui.announce_ota_file.clicked.connect(lambda: self.cmd_send('announce_ota_file'))
        self.ui.read_ota.clicked.connect(lambda: self.cmd_send('ota_read'))
        self.ui.write_ota.clicked.connect(lambda: self.cmd_send('ota_write'))
        # GeneralCommissioning
        self.ui.read_general_commissioning.clicked.connect(lambda: self.cmd_send('read_general_commissioning'))
        # Descriptor
        self.ui.read_discriptor.clicked.connect(lambda: self.cmd_send('descriptor_read'))
        # Diagnostics
        self.ui.read_sw_diagnostics.clicked.connect(lambda: self.cmd_send('read_sw_diagnostics'))
        self.ui.read_general_diagnostics.clicked.connect(lambda: self.cmd_send('read_general_diagnostics'))
        self.ui.read_wifinetworkdiagnostics.clicked.connect(lambda: self.cmd_send('read_wifinetworkdiagnostics'))
        # RO
        self.ui.read_user_label.clicked.connect(lambda: self.cmd_send('read_user_label'))
        self.ui.read_fixed_label.clicked.connect(lambda: self.cmd_send('read_fixed_label'))
        self.ui.read_lcfg.clicked.connect(lambda: self.cmd_send('read_lcfg'))
        self.ui.read_ltime.clicked.connect(lambda: self.cmd_send('read_ltime'))
        self.ui.read_timesync.clicked.connect(lambda: self.cmd_send('read_timesync'))
        # GRPKey
        self.ui.read_grpkey.clicked.connect(lambda: self.cmd_send('read_grpkey'))
        self.ui.key_set_write.clicked.connect(lambda: self.cmd_send('key_set_write'))
        self.ui.write_grpk_map.clicked.connect(lambda: self.cmd_send('write_group_key_map'))
        self.ui.add_group.clicked.connect(lambda: self.cmd_send('add_group'))
        self.ui.read_group_table.clicked.connect(lambda: self.cmd_send('read_group_table'))
        self.ui.view_group.clicked.connect(lambda: self.cmd_send('view_group'))
        self.ui.remove_group.clicked.connect(lambda: self.cmd_send('remove_group'))
        self.ui.get_group_membership.clicked.connect(lambda: self.cmd_send('get_group_membership'))
        self.ui.remove_all_group.clicked.connect(lambda: self.cmd_send('remove_all_groups'))
        self.ui.add_if_identify.clicked.connect(lambda: self.cmd_send('add_group_if_identifying'))
        # multi-fabrics
        self.manual_pairing_code_a, self.avahi_status, \
        self.node_label, self.commissioned = None, None, None, None
        self.open_commissioning_window = False
        self.fabrics, self.window_status, self.already_exists_error = None, True, False
        # DM
        self.ui.read_noc.clicked.connect(lambda: self.cmd_send('read_noc'))
        self.ui.read_nc.clicked.connect(lambda: self.cmd_send('read_nc'))
        self.ui.write_noc.clicked.connect(lambda: self.cmd_send('write_noc'))
        self.ui.write_nc.clicked.connect(lambda: self.cmd_send('write_nc'))
        self.ui.scan_networks.clicked.connect(lambda: self.cmd_send('scan_networks'))
        self.ui.arm_fail_safe.clicked.connect(lambda: self.cmd_send('arm_fail_safe'))
        self.ui.remove_network.clicked.connect(lambda: self.cmd_send('remove_network'))
        self.ui.add_update_wifi_nwk.clicked.connect(lambda: self.cmd_send('add_update_wifi_nwk'))
        self.ui.connect_network.clicked.connect(lambda: self.cmd_send('connect_network'))
        self.ui.reorder_network.clicked.connect(lambda: self.cmd_send('reorder_network'))
        self.ui.csrrequest.clicked.connect(lambda: self.cmd_send('csrrequest'))
        self.ui.attestation_request.clicked.connect(lambda: self.cmd_send('attestation_request'))
        self.ui.cert_chain_pai_request.clicked.connect(lambda: self.cmd_send('cert_chain_pai_request'))
        self.ui.cert_chain_dac_request.clicked.connect(lambda: self.cmd_send('cert_chain_dac_request'))
        self.ui.add_noc.clicked.connect(lambda: self.cmd_send('add_noc'))
        self.ui.update_noc.clicked.connect(lambda: self.cmd_send('update_noc'))
        self.ui.update_fabric_label.clicked.connect(lambda: self.cmd_send('update_fabric_label'))
        self.ui.remove_fabric.clicked.connect(lambda: self.cmd_send('remove_fabric'))
        self.ui.add_trusted_root_certificate.clicked.connect(lambda: self.cmd_send('add_trusted_root_certificate'))
        # Fabric
        self.ui.open_window.clicked.connect(lambda: self.cmd_send('open_window'))
        self.ui.fabric_pairing.clicked.connect(self.shared_dev_pairing)
        self.ui.avahi_browse_udp.clicked.connect(lambda: self.cmd_send('avahi_browse_udp'))
        self.ui.avahi_browse_tcp.clicked.connect(lambda: self.cmd_send('avahi_browse_tcp'))
        self.ui.read_fabrics.clicked.connect(lambda: self.cmd_send('read_fabrics'))
        self.ui.window_status.clicked.connect(lambda: self.cmd_send('window_status'))
        self.ui.read_fabrics_beta.clicked.connect(lambda: self.cmd_send('read_fabrics_beta'))
        self.secondary_node_id = None
        self.ui.ocw_bcm.clicked.connect(lambda: self.cmd_send('ocw_bcm'))
        self.ui.revoke_commissioning.clicked.connect(lambda: self.cmd_send('ocw_revoke'))
        self.ui.remove_fabrics.clicked.connect(lambda: self.cmd_send('remove_fabric'))
        self.ui.read_cadmin.clicked.connect(lambda: self.cmd_send('read_cadmin'))
        self.ui.ocw_done.clicked.connect(lambda: self.cmd_send('ocw_revoke'))
        # otbr
        self.ui.otbr_agent_state.clicked.connect(lambda: self.cmd_send('otbr_agent_state'))
        self.ui.otbr_agent_restart.clicked.connect(lambda: self.cmd_send('otbr_agent_restart'))
        self.ui.otbr_web_status.clicked.connect(lambda: self.cmd_send('otbr_web_state'))
        self.ui.mdns_status.clicked.connect(lambda: self.cmd_send('otbr_mdns_state'))
        self.ui.otbr_thread_start.clicked.connect(lambda: self.cmd_send('otbr_thread_start'))
        self.ui.otbr_dataset_active.clicked.connect(lambda: self.cmd_send('otbr_dataset_active'))
        self.ui.otbr_extpandid.clicked.connect(lambda: self.cmd_send('otbr_show_extpanid'))
        self.ui.otbr_get_pskc.clicked.connect(lambda: self.cmd_send('otbr_get_pskc'))
        self.ui.otbr_rcp_reset.clicked.connect(lambda: self.cmd_send('otbr_rcp_reset'))
        self.ui.otbr_factory_reset.clicked.connect(lambda: self.cmd_send('otbr_factory_reset'))
        # other
        self.ui.interactive_switch.clicked.connect(self.interactive_switch)

    def ui_operation(self, msg):
        match msg[1]:
            # QComboBox Operations
            case 'QComboBox.addItems':  # QComboBox addItems
                [msg[0].addItems(port for port in msg[2])]
            case 'QComboBox.addItem':  # QComboBox addItem
                msg[0].addItem(msg[2])
                msg[0].setCurrentText(msg[2])
            case 'QComboBox.clear':  # QComboBox clear
                # available
                msg[0].clear()
            case 'QComboBox.setCurrentText':
                msg[0].setCurrentText(msg[2])
            # QTextBrowser Operations
            case 'QTextBrowser.append':  # append text to QTextBrowser
                # available
                msg[0].append(msg[2])
                msg[0].verticalScrollBar().setValue(msg[0].verticalScrollBar().maximum() + 100)
            # QLineEdit Operations
            case 'QLineEdit.setText':  # QLineEdit.setText()
                # available
                msg[0].setText(msg[2])
            # QButton Operations
            case 'QButton.click':  # QButton.click()
                msg[0].click()
            case 'QButton.setText':  # QButton.setText()
                msg[0].setText(msg[2])
            # QListWidget Operations
            case 'QListWidget.addItems':
                msg[0].addItems(msg[2])
            # QStatusBar
            case 'QStatusBar.showMessage':
                msg[0].showMessage(msg[2])
            case _:
                pass

    def version_dialog(self):
        # Dialog show current Version
        vbox = QVBoxLayout()
        hbox = QHBoxLayout()
        panel = QLabel()
        panel.setText("Release Note:\r\n"
                      """
                     ## v2.4 Aug 22, 2023
                    ### v24_b Bug fixes or Improvements
                        v24_b.1 Fixed Nothing. Hopefully, the crash problem has been resolved
                    ### 24_f. New Feature
                        24_f.1 Descriptor Page, add 'EndPoint' element, choose endpoint for read command.
                        24_f.2 Using queue module in threaded method when information be exchanged wiht main thread
                        """
                      """

                                +-+-+-+ +-+-+-+ +-+-+-+-+-+ 
                                        |F|o|r| |t|h|e| |g|l|o|r|y| 
                                +-+-+-+ +-+-+-+ +-+-+-+-+-+ 

                      """
                      )
        hbox.addWidget(self.yes)
        vbox.addWidget(panel)
        vbox.addLayout(hbox)
        self.dialog.setLayout(vbox)
        self.dialog.setWindowModality(Qt.ApplicationModal)
        self.dialog.exec_()

    def connect_v2(self):
        self.dispatcher.put((self.ui.statusbar, 'QStatusBar.showMessage', 'Connecting to Ubuntu host ....waiting...'))
        QApplication.processEvents()
        self.ssh_client = paramiko.SSHClient()
        self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        raspberry_ip, ssh_port = self.ui.raspberry_ip.currentText(), self.ui.ssh_port.currentText()
        self.user_name, self.password = self.ui.ssh_user.currentText(), self.ui.ssh_psk.currentText()
        login_prompt = ''
        try:
            self.ssh_client.connect(raspberry_ip, ssh_port, self.user_name, self.password, timeout=5)
            self.ssh_channel = self.ssh_client.invoke_shell(term='xterm', width=40960, )  # 交互式shell窗口
            self.ssh_channel.settimeout(5)
            if self.ssh_channel.active:
                login_prompt = self.ssh_channel.recv(4096).decode('utf-8')
                # 删除Ubuntu系统返回字符中的颜色代码
                # \x1b[01;34m  这个应该是表示颜色的，
                # \x1b[0m  这个是结果样式的标志，代表：关闭所有属性
                # \x1b是十六进制Esc（ASCII码27）
                pattern = r'\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))'
                login_prompt_cleaned = re.sub(pattern, '', login_prompt)
                self.ui.log_printer.append(login_prompt_cleaned)
        except Exception as E:
            self.ui.log_printer.append(str(E))
            self.ui.log_printer.append(traceback.format_exc())
        try:
            if 'Welcome to Ubuntu' in login_prompt or 'Last login' in login_prompt:
                self.ssh_connect_flag = True
                self.dispatcher.put((self.ui.statusbar, 'QStatusBar.showMessage', 'Chip-tool host Connected'))
                self.ui.app_path.setText(f'/home/{self.user_name}/apps/')
                self.ui.cmd_input.setText(f'/home/{self.user_name}/apps/chip-tool')
                self.chip_app_path = f'/home/{self.user_name}/apps/'
                self.path_confirmed = True

                # Launch msg_parser thread
                self.parser = threading.Thread(target=self.msg_parser)
                self.parser.daemon = True
                self.parser.start()
                self.ui.connect.setIcon(qta.icon('mdi.lan-connect', color='gray'))
                self.ui.connect.setEnabled(False)
                self.ui.disconnect.setIcon(qta.icon('mdi.lan-disconnect', color='red'))
                self.ui.disconnect.setEnabled(True)
                self.node_query()
            else:
                self.ui.log_printer.append('Fail to init connection, timeout !')
                return
            # v2.1:
            # check paa-path exists or not
            # update --paa-trust-store-path by user-name
            gevent.joinall([
                gevent.spawn(self.cmd_send('check_paa_path')),
            ])
        except Exception as E:
            self.ui.log_printer.append(str(E))
            self.ui.log_printer.append(traceback.format_exc())

    def disconnect(self):
        # 断开连接，清理线程
        if not self.ssh_connect_flag:
            self.ui.log_printer.setText('Connection not active')
            return
        if self.parser.is_alive():
            self.ssh_connect_flag = False   # thread ssh_resp_collector will check this attribute, if False then break
            time.sleep(0.2)
            self.ssh_client.close()
            self.ui.connect.setIcon(qta.icon('mdi.lan-connect', color='green'))
            self.ui.connect.setEnabled(True)
            self.ui.disconnect.setEnabled(False)
            self.dispatcher.put((self.ui.statusbar, 'QStatusBar.showMessage', 'Be prepared, here we go !'))

    def closeEvent(self, event):
        # 退出前清理子线程及定时器， 貌似还没有待清理的
        app.exec_()

    def pair_mode(self):
        mode = self.ui.pairing_mode.currentText()
        if mode == 'ble-wifi':
            # ble-wifi device
            self.ui.operational_dataset.setDisabled(True)
            self.ui.wifi_ssid.setEnabled(True)
            self.ui.wifi_psk.setEnabled(True)
            self.ui.discriminator.setEnabled(True)
        if mode == 'ble-thread':
            # ble-thread device
            self.ui.operational_dataset.setEnabled(True)
            self.ui.wifi_ssid.setEnabled(False)
            self.ui.wifi_psk.setEnabled(False)
            self.ui.discriminator.setEnabled(True)
            #self.cmd_send_test('sudo ot-ctl dataset active -x')
            self.cmd_send_test('otbr_dataset_active')
        if mode == 'onnetwork':
            # If you want to test ota-firmware-upgrade, you neet to pair ubuntu host which chip-tool running to fabric
            # with type 'onnetwork'
            self.ui.operational_dataset.setEnabled(False)
            self.ui.wifi_ssid.setEnabled(False)
            self.ui.wifi_psk.setEnabled(False)
            self.ui.discriminator.setEnabled(False)

    def dut_pairing(self):
        if not self.ssh_channel:
            self.ui.log_printer.append('ssh 未连接')
            return
        pairing_mode, node_id = self.ui.pairing_mode.currentText(), self.ui.node_id_assign.text()
        if not node_id.isdecimal():
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append',
                                 'invalid node_id, it should be decimal format'))
            return
        if pairing_mode not in ['ble-wifi', 'ble-thread', 'onnetwork']:
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append',
                                 'pairing mode not support yet, ble-wifi/ble-thread/onnetwork'))
            return
        setup_pin_code, discriminator = self.ui.setup_code.text(), self.ui.discriminator.text()
        option_parameter = self.ui.option_parameter.currentText()

        if not setup_pin_code.isdecimal() or not node_id.isdecimal():
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append',
                                 '请正确输入dut的setup-pin-code & node_id'))
            return

        if option_parameter == 'option_parameter' or option_parameter == '':
            option_parameter = ''

        # ble-wifi mode
        cmd_pairing = ''
        # option_parameter 已带前置空格， cmd拼接时不再加空格
        if pairing_mode == 'ble-wifi':
            if not discriminator.isdecimal():
                self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append',
                                     '请正确输入dut的discriminator'))
                return
            wifi_ssid, wifi_psk = self.ui.wifi_ssid.text(), self.ui.wifi_psk.text()
            if wifi_ssid == 'target_wifi_ssid' or wifi_ssid == '' or wifi_psk == 'target_wifi_psk':
                self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append',
                                     '请输入dut待接入的 ssid 及 psk'))
                return
            cmd_pairing = f'{self.chip_app_path}chip-tool pairing {pairing_mode} {node_id} {wifi_ssid} {wifi_psk} ' \
                          f'{setup_pin_code} {discriminator}{option_parameter}'
        # ble-thread mode
        if pairing_mode == 'ble-thread':
            optional_key = self.ui.operational_dataset.text()
            if optional_key == 'operational_dataset' or optional_key == '':
                self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append',
                                     'operational key not valid'))
                return
            cmd_pairing = f'{self.chip_app_path}chip-tool pairing {pairing_mode} {node_id} hex:{optional_key} ' \
                          f'{setup_pin_code} {discriminator}{option_parameter}'
        # onnetwork
        # for ota_provider , ota file should be specified in advance
        if pairing_mode == 'onnetwork':
            cmd_pairing = f'{self.chip_app_path}chip-tool pairing {pairing_mode} {node_id} ' \
                          f'{setup_pin_code}{option_parameter}'

        self.cmd_send(cmd_pairing)

        if node_id not in self.node_list:
            # 新增dut node id 写入cfg\dev.ini, 刷新列表 dut_node_id
            self.cfg.set('dev_node_id', f'dev{str(self.dev_num + 1)}', node_id)
            with open(self.cfg_path, 'w') as cfg_file:
                self.cfg.write(cfg_file)
                cfg_file.close()
            self.dev_num += 1
            self.node_list.insert(0, node_id)
        # Set the new dut to the top position
        self.node_list.remove(node_id)
        self.node_list.insert(0, node_id)

        self.ui.dut_node_id.clear()
        QApplication.processEvents()
        [self.ui.dut_node_id.addItem(node_id) for node_id in self.node_list]
        # self.ui.dut_node_id.setCurrentText(node_id)
        QApplication.processEvents()

    def shared_dev_pairing(self):
        _id = self.ui.node_id_fabric.text()
        _p_code = self.ui.fabric_payload.text()
        _c_name = self.ui.commissioner_name.currentText()
        _paa = self.ui.paa_path.currentText()
        _paa_path_list = {'ack': '/home/ubuntu/apps/ack_paa',
                          'sengled': '/home/ubuntu/apps/sengled_paa',
                          'sengled_test': '/home/ubuntu/apps/sengled_test_paa'
                          }
        if not _id.isdecimal() or not _p_code.isdecimal():
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', 'in valid Node_ID or ManualPairingCode'))
            return

        if _paa == 'no_paa':
            _cmd = f'{self.chip_app_path}chip-tool pairing code {_id} {_p_code} --commissioner-name {_c_name}'
        else:
            _cmd = (f'{self.chip_app_path}chip-tool pairing code {_id} {_p_code} --commissioner-name {_c_name}  '
                    f'--paa-trust-store-path {_paa_path_list[_paa]}')
        _shared_id = f'{_id}_{_c_name}'
        if _shared_id not in self.node_list:
            # 新增dut node id 写入cfg\dev.ini, 刷新列表 dut_node_id
            self.cfg.set('dev_node_id', f'dev{str(self.dev_num + 1)}', _shared_id)
            with open(self.cfg_path, 'w') as cfg_file:
                self.cfg.write(cfg_file)
                cfg_file.close()
            self.dev_num += 1
            # self.node_list.append(self.secondary_node_id)
            self.node_list.insert(0, _shared_id)
        self.node_list.remove(_shared_id)
        self.node_list.insert(0, _shared_id)
        self.ui.dut_node_id.clear()
        [self.ui.dut_node_id.addItem(item) for item in self.node_list]
        try:
            if self.ssh_channel.send_ready():
                self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append',
                                     f"\n<<  {_cmd.replace('/home/ubuntu/apps/chip-tool >>', '')}"))
                self.ssh_channel.send(_cmd + '\n')
        except Exception as E:
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', str(E)))
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', str(traceback.format_exc())))

    def dut_unpair(self):
        dut_node_id = self.ui.dut_node_id.currentText()
        node_id_index = self.ui.dut_node_id.currentIndex()
        fabric_index = '1'  # TODO: If chip-tool is the first commissioner, fabric_index is '1', otherwise not
        if dut_node_id == '':
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', 'All devices removed'))
            return
        try:
            # remove from chiptool local db, /tmp/xxx.csv
            self.cmd_send_test(f'{self.chip_app_path}chip-tool pairing unpair {dut_node_id}')
            # remove fabric, dut will reset to factory new state
            self.cmd_send_test(f'{self.chip_app_path}chip-tool operationalcredentials remove-fabric {fabric_index} '
                               f'{dut_node_id} 0')
            self.ui.dut_node_id.removeItem(node_id_index)
            self.dev_num -= 1
            self.node_list.remove(dut_node_id)
            QApplication.processEvents()
            with open(self.cfg_path, 'r') as dev_list_file:
                lines = dev_list_file.readlines()
                dev_list_file.close()
            with open(self.cfg_path, 'w') as dev_list_file:
                for line in lines:
                    if dut_node_id not in line:
                        dev_list_file.write(line)
                dev_list_file.close()
        except Exception as E:
            self.ui.log_printer.append(str(E))
            traceback.print_exc()
            self.ui.log_printer.append(traceback.format_exc())

    def cmd_send(self, instruction: str) -> None:
        cmd_send_out(disp=self.dispatcher, ui=self.ui, ssh_ch=self.ssh_channel,
                 chip_path=self.chip_app_path, cmd=instruction)

    def msg_parser(self):
        import time
        """
         SSH连接消息监听器，connect时加载线程, 实时解析
        """
        t = 0.25
        while True:
            time.sleep(t)
            if self.ssh_channel.closed:
                self.dispatcher.put((self.ui.log_printer, 'QTextBrowser.append', 'ERROR, SSH Channel Closed'))
                self.ui.connect.setIcon(qta.icon('mdi.lan-connect', color='green'))
                self.ui.connect.setEnabled(True)
                self.ui.disconnect.setIcon(qta.icon('mdi.lan-disconnect', color='gray'))
                self.ui.disconnect.setEnabled(False)
                break
            message = ''
            # msg format
            try:
                if self.ssh_channel.recv_ready():
                    # Returns true if data is buffered and ready to be read from this channel.
                    # A False result does not mean that the channel has closed; it means you may need to wait before
                    # more data arrives.
                    # If recv() directly without assertion recv_ready(), if no data is ready before the timeout set by
                    # settimeout(), socket.timeout will be raised.
                    message = self.ssh_channel.recv(65535).decode('utf-8')
                else:
                    continue
            except Exception as E:
                self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', traceback.format_exc()))
            if len(message) == 0:
                continue
            if message is None:
                continue
            # 删除IRC颜色控制码
            pattern = r'\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))'
            msg = re.sub(pattern, '', message)
            if not isinstance(msg, str):
                msg = str(msg)
            # keywords parse
            self.dispatcher.put((self.ui.log_printer, 'QTextBrowser.append',
                                 '-------- Original Log --------\n' + msg + '\n' + '--' * 50))
            self.cm_step = msg_parser(dispatcher=self.dispatcher, ui=self.ui, ssh_ch=self.ssh_channel,
                                      cm_step=self.cm_step ,msg=msg)
            # >>>>>>>>>>>>>>>>>>>>>>Parser<<<<<<<<<<<<<<<<<<<<<<<<
            # keynote 自动滚屏游标到底
            time.sleep(0.25)
            self.ui.keynote.verticalScrollBar().setValue(
                self.ui.keynote.verticalScrollBar().maximum())
            self.ui.log_printer.verticalScrollBar().setValue(
                self.ui.log_printer.verticalScrollBar().maximum())
            QApplication.processEvents()

    def attrib_write(self, cluster_name: str, attrib_name: str, value: str, endpoint: str) -> None:
        if attrib_name == '0xFFFFFFFF':
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', 'Wildcard attribute name is not avalid'))
            return
        if value == '':
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', 'attribute value should not be Empty'))
            return
        _clu, _att, _val, _ep = cluster_name, attrib_name, value, endpoint
        _node_id = self.ui.dut_node_id.currentText()
        _commissioner_name = re.findall('_(.*)', _node_id)
        _dst, _c_name = _node_id, None
        self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append',
                             f'<< Modify the attribute {_att} of cluster {_clu} to {_val} >>'))
        _cmd = f'{self.chip_app_path}chip-tool {_clu} write {_att} {_val} {_dst} {_ep}\n'
        # for multi-fabric dut
        if len(_commissioner_name) > 0:
            _dst = _node_id.replace(f'_{_commissioner_name[0]}', '')
            _c_name = _commissioner_name[0]
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append',
                                 f'<<-- {_clu} write {_att} '))
            _cmd = (f'{self.chip_app_path}chip-tool {_clu} write {_att} {_val} {_dst} {_ep} '
                    f'--commissioner-name {_c_name}\n')
        try:
            self.ssh_channel.send(_cmd)
        except Exception as E:
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', str(traceback.format_exc())))
            if str(E) == 'Socket is closed':
                try:
                    self.connect_v2()
                except Exception as E:
                    self.disconnect()
                    self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', str(traceback.format_exc())))

    def active_ota_file(self):
        """
        1.chip-ota-provider-app 需起线程单独运行，启动后暂时无法终止，通过ps aux /top 可查验服务是否拉起
        2.检测服务状态通过cmd_send_test主线程执行，见if cmd=='active_ota_file'
        :return:
        """
        if not self.ota_bin_file:
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', 'matter-ota.bin file does not exist'))
            return
        remote_path = f'{self.chip_app_path}ota_path/{self.ota_bin_file}'
        self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', f'>>>>>{remote_path}<<<<<<'))
        ssh_cannon_fodder = paramiko.SSHClient()
        ssh_cannon_fodder.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        raspberry_ip, ssh_port = self.ui.raspberry_ip.currentText(), self.ui.ssh_port.currentText()
        username, passwd = self.user_name, self.password
        try:
            ssh_cannon_fodder.connect(raspberry_ip, ssh_port, username, passwd)
            ssh_cannon_fodder_channel = ssh_cannon_fodder.invoke_shell(term='xterm', width=40960,)  # 交互式shell窗口
            ssh_cannon_fodder_channel.settimeout(5000)
            time.sleep(3)
            cmd = f'\n{self.chip_app_path}chip-ota-provider-app -f {remote_path}\n'
            self.dispatcher.put((self.ui.log_printer, 'QTextBrowser.append', f'>>>>>{cmd}<<<<<'))
            ssh_cannon_fodder_channel.send(bytes(cmd.encode('utf-8')))
            time.sleep(2)
            while self.ota_session_flag is True:
                if self.ota_session_flag is False:
                    self.dispatcher.put((self.ui.log_printer, 'QTextBrowser.append', 'ota_session_flag is False'))
                    return
                try:
                    time.sleep(2.5)
                    ota_info = ssh_cannon_fodder_channel.recv(40960).decode('utf-8')
                    #if 'CHIP' in ota_info:
                    if len(ota_info) > 0:
                        self.dispatcher.put((self.ui.log_printer, 'QTextBrowser.append',
                                             f'-------- Original Log -------->>O>>T>>A>>I>>N>>F>>O:\n'
                                             f'{ota_info}\n<<<<<<<<<<<<<<<<Msg-of-thread\n'))
                        QApplication.processEvents()
                        if 'Using OTA file' in ota_info:
                            # 'Using OTA file' in ota_info means ota_provider_app launched,
                            # switch to self.ota_provider_check()
                            self.dispatcher.put((self.ui.log_printer, 'QTextBrowser.append',
                                                 'get target msg, switch to ota-provider-status-check'))

                            self.ui.active_status.setText('Activated')
                            self.ui.active_status.setStyleSheet('background-color: rgb(0, 255, 0);')
                            gevent.sleep(0)
                    else:
                        continue
                except Exception as E:
                    self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', str(traceback.format_exc())))
                    continue
        except Exception as E:
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', str(traceback.format_exc())))
            return
        self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', 'Exception then ssh-closed'))
        ssh_cannon_fodder_channel.close()

    def provider_status_check(self):
        # use gevent.spawn to make sure the method self.provider_status_check() be executed behind self.active_ota_file()
        self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append',
                             f'A new thread will be create to launch chip-ota-provider-app, '
                             f'and {self.ui.ota_file_server.text()} will be activated. '
                             f'It need several times to commplete.'))
        QApplication.processEvents()
        gevent.sleep(0)  # swith to the method self.active_ota_file()
        self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', 'Get the right back'))
        cmd = '\nps -ax|grep chip-ota-provider-app\n'
        self.cmd_send(cmd)

    def ota_file_query(self):
        # todo
        # Automatically scan and display the matter ota.bin file while entering the ota page
        if self.ui.tabWidget.currentIndex() != 10:
            return
        self.ui.ota_file_server.clear()
        self.ui.active_status.setText('unknown')
        self.ui.active_status.setStyleSheet('background-color: white;')
        self.ui.acl_status.setText('unknown')
        self.ui.acl_status.setStyleSheet('background-color: white;')
        self.cmd_send(f'ls {self.chip_app_path}ota_path/')
        time.sleep(1)
        self.ota_queried = True
        ota_help = """
            OTA Operation Stages Prompt
            a). Uploads ota.bin file and activates it.
            b). Commission Chip-tool host by 'onnetwrok' mode, with node-id '110'.
            c). Complete the 'ACL' settings.
            d). Commission Dut to the same Fabric.
            e). Send AnnounceOtaProviderCommand to Dut, provider-id is '110'.
        """
        self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', ota_help))

    def node_query(self):
        # ssh 连接建立后初始化设备清单及paa-store-path
        # 1.查询本地dev.ini查询子设备

        self.cfg = configparser.ConfigParser()
        self.cfg.read(self.cfg_path, encoding='utf-8')
        items = self.cfg.items('dev_node_id')
        self.dev_num = len(items)
        [self.node_list.append(dev[1]) for dev in items]
        [self.ui.dut_node_id.addItem(dev) for dev in self.node_list]
        # 2.根据登录user-name 初始化 --paa-trust-store-path
        with open(self.paa_cfg_file, 'w') as paa_list_file:
            vendors = ['ack_paa', 'sengled_paa', 'sengled_test_paa']
            #[paa_list_file.write(f' --paa-trust-store-path /home/{self.user_name}/apps/{vid}/') for vid in vendors]
            [self.ui.option_parameter.addItem(f' --paa-trust-store-path /home/{self.user_name}/apps/{vid}/') for vid in vendors]

    def interactive_switch(self):
        _state = self.ui.interactive_switch.text()
        _cmd = ''
        if _state == 'interactive off':  # 开启交互模式
            _cmd = f'{self.chip_app_path}chip-tool interactive start'
            self.ui.interactive_switch.setText('interactive on')
        else:
            _cmd = '\x03'  # \x03 = ctrl+c , 退出交互模式
            self.ui.interactive_switch.setText('interactive off')
        try:
            if self.ssh_channel.send_ready():
                self.ssh_channel.send(_cmd + '\n')
        except Exception as E:
            self.dispatcher.put((self.ui.keynote, 'QTextBrowser.append', str(traceback.format_exc())))

    def interactive_cmd(self, cmd):
        pass


if __name__ == "__main__":
    import sys
    import os

    # print switch
    # 恢复标准输出流
    sys.stdout = sys.__stdout__
    # 重定向标准输出流
    # sys.stdout = open('logs/null', 'w')

    # dev list
    if not os.path.exists('./cfg'):
        os.mkdir('./cfg')

    app = QApplication(sys.argv)
    app.processEvents()
    scale_rate = app.screens()[0].logicalDotsPerInch()
    ui = MatterController()
    ui.setWindowIcon(QIcon(r".\images\matter.png"))
    ui.show()

    sys.exit(app.exec_())
